/** * Copyright 2012-2017 Gunnar Morling (http://www.gunnarmorling.de/) * and/or other contributors as indicated by the @authors tag. See the * copyright.txt file in the distribution for a full listing of all * contributors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.mapstruct.eclipse.internal.proposal.visitors; import static org.mapstruct.eclipse.internal.MapStructAPIConstants.CONTEXT_FQ_NAME; import static org.mapstruct.eclipse.internal.MapStructAPIConstants.MAPPING_FQ_NAME; import static org.mapstruct.eclipse.internal.MapStructAPIConstants.MAPPING_MEMBER_SOURCE; import static org.mapstruct.eclipse.internal.MapStructAPIConstants.MAPPING_MEMBER_TARGET; import static org.mapstruct.eclipse.internal.MapStructAPIConstants.MAPPING_TARGET_FQ_NAME; import static org.mapstruct.eclipse.internal.MapStructAPIConstants.TARGET_TYPE_FQ_NAME; import static org.mapstruct.eclipse.internal.MapStructAPIConstants.VALUE_MAPPING_FQ_NAME; import static org.mapstruct.eclipse.internal.util.Bindings.containsAnnotation; import static org.mapstruct.eclipse.internal.util.Bindings.getAnnotationQualifiedName; import java.beans.Introspector; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Deque; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.TreeSet; import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.IAnnotationBinding; import org.eclipse.jdt.core.dom.IMethodBinding; import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.core.dom.IVariableBinding; import org.eclipse.jdt.core.dom.MemberValuePair; import org.eclipse.jdt.core.dom.MethodDeclaration; import org.eclipse.jdt.core.dom.SingleVariableDeclaration; import org.mapstruct.eclipse.internal.util.Bindings; import org.mapstruct.eclipse.internal.util.Ranges; /** * AST Visitor that collects proposals for source/target property paths * * @author Andreas Gudian */ public class PropertyNameProposalCollector extends ASTVisitor { private static final String[] READ_ACCESSOR_PREFIXES = { "get", "is" }; //$NON-NLS-1$ private static final String[] WRITE_ACCESSOR_PREFIXES = { "set" }; //$NON-NLS-1$ private final int invocationOffset; private final String givenPrefix; private final Collection<String> proposedProperties = new TreeSet<String>(); private final Map<String, ITypeBinding> sourceNameToType = new HashMap<String, ITypeBinding>(); private boolean source = false; private boolean valid = false; private boolean inMethod = false; private ITypeBinding resultType; private String proposalPrefix; /** * @param invocationOffset cursor position * @param givenPrefix value that is already present (before the cursor) */ public PropertyNameProposalCollector(int invocationOffset, String givenPrefix) { super( false ); this.invocationOffset = invocationOffset; this.givenPrefix = givenPrefix; } @Override public boolean visit(MemberValuePair node) { String annotationQualifiedName = getAnnotationQualifiedName( node.resolveMemberValuePairBinding() ); if ( isSupportedAnnotation( annotationQualifiedName ) && Ranges.isInRange( invocationOffset, node.getValue().getStartPosition(), node.getValue().getLength() ) && ( isSourceNode( node ) || isTargetNode( node ) ) ) { valid = true; source = isSourceNode( node ); } return false; } private boolean isSupportedAnnotation(String annotationQualifiedName) { return MAPPING_FQ_NAME.equals( annotationQualifiedName ) || VALUE_MAPPING_FQ_NAME.equals( annotationQualifiedName ); } @Override public boolean visit(MethodDeclaration node) { if ( Ranges.isInRange( invocationOffset, node.getStartPosition(), node.getLength() ) ) { inMethod = true; return true; } inMethod = false; return false; } @Override public boolean visit(SingleVariableDeclaration node) { IVariableBinding binding = node.resolveBinding(); IAnnotationBinding[] annotations = binding.getAnnotations(); if ( containsAnnotation( annotations, MAPPING_TARGET_FQ_NAME ) ) { resultType = binding.getType(); } else if ( !containsAnnotation( annotations, TARGET_TYPE_FQ_NAME ) && !containsAnnotation( annotations, CONTEXT_FQ_NAME ) ) { sourceNameToType.put( node.getName().toString(), binding.getType() ); } return false; } @Override public void endVisit(MethodDeclaration node) { if ( !inMethod ) { return; } String pathWithoutLastElement = getPathWithoutLastElement( givenPrefix ); Deque<String> pathToProposedType; if ( pathWithoutLastElement.isEmpty() ) { pathToProposedType = new LinkedList<String>(); proposalPrefix = ""; } else { pathToProposedType = new LinkedList<String>( Arrays.asList( pathWithoutLastElement.split( "\\." ) ) ); proposalPrefix = pathWithoutLastElement + "."; } String propertyPrefix = getLastPathElement( givenPrefix ); ITypeBinding proposalType = getTypeForPropertyProposals( node, pathToProposedType, propertyPrefix ); if ( proposalType != null ) { proposePropertiesIfPrefixMatches( propertyPrefix, proposalType ); } } private ITypeBinding getTypeForPropertyProposals(MethodDeclaration node, Deque<String> pathToProposedType, String propertyPrefix) { ITypeBinding proposalType; if ( source ) { if ( sourceNameToType.size() > 1 ) { if ( pathToProposedType.isEmpty() ) { proposeIfPrefixMatches( propertyPrefix, sourceNameToType.keySet() ); return null; } else { // for multiple source params, the first element would be expected to be the parameter name String first = pathToProposedType.removeFirst(); proposalType = sourceNameToType.get( first ); } } else { proposalType = sourceNameToType.values().iterator().next(); } } else { if ( resultType == null ) { proposalType = node.resolveBinding().getReturnType(); } else { proposalType = resultType; } } return retrieveProposalTypeFromPath( proposalType, pathToProposedType ); } private ITypeBinding retrieveProposalTypeFromPath(ITypeBinding proposalType, Deque<String> pathToProposedType) { if ( proposalType != null && !pathToProposedType.isEmpty() ) { if ( proposalType.isEnum() ) { return null; } String propertyName = pathToProposedType.removeFirst(); Collection<IMethodBinding> methodNames = Bindings.findAllMethods( proposalType ); String[] prefixes = source ? READ_ACCESSOR_PREFIXES : WRITE_ACCESSOR_PREFIXES; Map<String, List<IMethodBinding>> propertyMethods = findPropertyMethods( propertyName, methodNames, prefixes ); if ( !propertyMethods.isEmpty() ) { IMethodBinding firstMethod = propertyMethods.values().iterator().next().iterator().next(); ITypeBinding nextType; if ( source ) { nextType = firstMethod.getReturnType(); } else { nextType = ( firstMethod.getParameterTypes().length > 0 ? firstMethod.getParameterTypes()[0] : firstMethod.getReturnType() ); } proposalType = retrieveProposalTypeFromPath( nextType, pathToProposedType ); } else { return null; } } return proposalType; } private void proposePropertiesIfPrefixMatches(String propertyPrefix, ITypeBinding type) { if ( type.isEnum() ) { proposeIfPrefixMatches( propertyPrefix, Bindings.findAllEnumConstants( type ) ); } else { Collection<IMethodBinding> methodNames = Bindings.findAllMethods( type ); if ( source ) { proposeIfPrefixMatches( propertyPrefix, findPropertyMethods( methodNames, READ_ACCESSOR_PREFIXES ).keySet() ); } else { proposeIfPrefixMatches( propertyPrefix, findPropertyMethods( methodNames, WRITE_ACCESSOR_PREFIXES ).keySet() ); } } } private void proposeIfPrefixMatches(String propertyPrefix, Collection<String> keySet) { for ( String value : keySet ) { proposeIfPrefixMatches( propertyPrefix, value ); } } private void proposeIfPrefixMatches(String prefix, String value) { if ( prefix.isEmpty() || value.startsWith( prefix ) ) { proposedProperties.add( proposalPrefix + value ); } } private static boolean isTargetNode(MemberValuePair node) { return MAPPING_MEMBER_TARGET.equals( node.getName().toString() ); } private static boolean isSourceNode(MemberValuePair node) { return MAPPING_MEMBER_SOURCE.equals( node.getName().toString() ); } /** * Finds {@link IMethodBinding}s starting with any of the given prefixes for the given property. */ private static Map<String, List<IMethodBinding>> findPropertyMethods(String propertyName, Collection<IMethodBinding> methods, String... candidatePrefixes) { Map<String, List<IMethodBinding>> returnValue = new HashMap<String, List<IMethodBinding>>(); for ( IMethodBinding method : methods ) { String methodName = method.getName(); String matchingPrefix = getMatchingPrefix( methodName, candidatePrefixes ); if ( matchingPrefix != null ) { String methodPropertyName = Introspector.decapitalize( methodName.substring( matchingPrefix.length() ) ); if ( propertyName == null || methodPropertyName.equals( propertyName ) ) { List<IMethodBinding> accessorMethods = returnValue.get( methodPropertyName ); if ( accessorMethods == null ) { accessorMethods = new ArrayList<IMethodBinding>( 2 ); returnValue.put( methodPropertyName, accessorMethods ); } accessorMethods.add( method ); } } } return returnValue; } /** * Finds {@link IMethodBinding}s starting with any of the given prefix and extracts the associated property name * from it. */ private static Map<String, List<IMethodBinding>> findPropertyMethods(Collection<IMethodBinding> methods, String[] candidatePrefixes) { return findPropertyMethods( null, methods, candidatePrefixes ); } private static String getMatchingPrefix(String methodName, String[] candidatePrefixes) { for ( String prefix : candidatePrefixes ) { if ( methodName.startsWith( prefix ) ) { return prefix; } } return null; } private static String getLastPathElement(String path) { int lastDot = path.lastIndexOf( '.' ); if ( lastDot >= 0 ) { return path.substring( lastDot + 1, path.length() ); } return path; } private static String getPathWithoutLastElement(String path) { int lastDot = path.lastIndexOf( '.' ); if ( lastDot >= 0 ) { return path.substring( 0, lastDot ); } return ""; } public Collection<String> getProperties() { return proposedProperties; } public boolean isValidValue() { return valid; } }